/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package geneontofx.math;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.HashMap;

/**
 *
 * @author loskov
 */
public class Factorial {
    
    private static MathContext MATH_CONTEXT = new MathContext(16, RoundingMode.CEILING);
    private static HashMap<Integer, BigDecimal> factorialDecimalCacheMap = new HashMap<>();
    private static HashMap<Integer, BigDecimal> factorialLogCacheMap = new HashMap<>();
    private static HashMap<String, BigDecimal> pochhammerCacheMap = new HashMap<>();
    
    public static enum FactorialType {
        DECIMAL, LOG, POCHHAMMER
    }
    
    private BigDecimal result;
    private FactorialType type;

    public Factorial() {
        
    }

    @Override
    public String toString() {
        //TODO factorial toString
        return type + " Factorial {" + "result=" + result + '}';
    }
    
    public BigDecimal factLogCached(Integer n) {
        if(factorialLogCacheMap.containsKey(n)) {
            return factorialLogCacheMap.get(n);
        }
        else {
            result = new BigDecimal(0);
            for(int i = 1; i <= n; i++) {
                result = result.add(new BigDecimal(Math.log(i)));
                factorialLogCacheMap.put(i, result);
            }
            type = FactorialType.LOG;
            return result;
        }
        
    }

    public BigDecimal factDecimalCached(Integer n) {
        type = FactorialType.DECIMAL;
        if(factorialDecimalCacheMap.containsKey(n)) {
            return factorialDecimalCacheMap.get(n);
        }
        else {
            if(n.intValue() < 0) {
                //TODO fact(n) for n<0 logger
                return null;
            }
            else if(n == 0 || n == 1) {
                factorialDecimalCacheMap.put(n, BigDecimal.ONE);
                return BigDecimal.ONE;
            }
            else if(n.intValue() < 200) {
                BigDecimal nDec = new BigDecimal(n);
                BigDecimal result = nDec.multiply(factDecimalCached(n-1));
                factorialDecimalCacheMap.put(n, result);
                return result;
            }
            else {
                // Stirling's formula: n! ~ (n/e)^n * sqrt(2*pi*n)
                // for n > 200 error ~ 0,04 %
                BigDecimal nDec = new BigDecimal(n);
                BigDecimal eDec = new BigDecimal(Math.E);
                BigDecimal nDivE = nDec.divide(eDec, MATH_CONTEXT);
                BigDecimal powerPart = nDivE.pow(n.intValue());
                BigDecimal sqrtPart = new BigDecimal(Math.sqrt(2*Math.PI*n.doubleValue()));
                BigDecimal result = powerPart.multiply(sqrtPart);
                factorialDecimalCacheMap.put(n, result);
                return result;
            }
        }
    }
    
    public BigDecimal pochhammerFallFact(Integer n, Integer k) {
        type = FactorialType.POCHHAMMER;
        String pochFactId = n + "," + k;
        if(pochhammerCacheMap.containsKey(pochFactId)) {
            return pochhammerCacheMap.get(pochFactId);
        }
        else {
            BigDecimal pochFact = new BigDecimal(1);
            BigDecimal xDec = new BigDecimal(n);
            BigDecimal step;

            for(int i = 1; i <= k; i++) {
                step = xDec.add(new BigDecimal(1-i));
                pochFact = pochFact.multiply(step);
            }


            pochhammerCacheMap.put(pochFactId, pochFact);
            return pochFact;
        }
        
    }
    
}
